// -----------------------------------------
// fuchsia.js
// Requires jQuery 1.3.1 (http://www.jquery.com/), plus the jquery.ui and jquery.domec plugins
// AndrewJ 2006-12-29
// -----------------------------------------

// -----------------------------------------
// Object: info
var Info = {
	name: "fuchsia",
	author: "AndrewJ", 
	version: "0.3.9",
	date: "2008-12-24",
	info: "<p>Fuchsia is a 'micro web app' for simple idea capture and free-form association. It can be used to assemble fragments into structures, like sticky notes on a wall.</p><p>The <em>import/export</em> functions allow you to cut and paste the notes into a file and then paste them back to the canvas in a separate session. JSON format preserves position and colour. The <em>snapshot</em> function requires Google Gears to be installed.  It stores the current notes on your local computer.</p><p>Some ideas for use:<ul><li>Use it for brainstorming and forming <a href='http://mot.vuse.vanderbilt.edu/mt322/Affinity.htm'>affinity diagrams</a></li><li>Simulate Post-it notes, as in the book <em>Rapid Problem Solving with Post-It Notes</em> by David Straker.</li><li>Make 'magnetic poetry' with a list of common words</li><li>Use random words, layout, and colours for creative idea generation and problem solving, as in the book <em>How to Have Creative Ideas</em>, by Edward de Bono.</li></ul></p><p>This software is released under the <a href='http://creativecommons.org/'>Creative Commons</a> Attribution-ShareAlike 2.5 license. It uses the excellent <a href='http://jquery.com/'>jQuery</a> JavaScript libraries, and the Wandering Horse <a href='http://wanderinghorse.net/computing/javascript/jquery/colorpicker/'>jQuery color picker</a>.</p>",
	appendTo: function(tagName) {
		var desc = $.create("span", {}, [this.version]);
		var title = $.create("span", { "class": "title" }, [this.name]);
		var str = $.create("p", {}, [title, desc]);
		$(tagName).append(str);
	},
	aboutText: function() {
		str = "<p><em>" + this.name + " v" + this.version;
		str += ", last modified: " + this.date;
		str += " by: " + this.author + "</em></p>";
		str += this.info;
		return str;
	}
}

// -----------------------------------------
// Class: command menu
function CommandMenu(cmds, arg) {
	// public methods
	this.commands = cmds || [];
	this.arg = arg || '';
	my = this;
	
	this.createList =  function (arr, className) {
		var cmds = [];
		arr.each( function (i, item) {
			var e = [];
			if (item.list != null) {
				var ul = that.createList(item.list, "subnavlist");
				e.push($.create("span", { "class": "button", "title": item.title}, [item.label]));
				e.push(ul);
			} else
				e.push($.create("span", { "class": "button", "onClick": item.command + "('" + my.arg + "');", "title": item.title, "id": item.id }, [item.label]));
			cmds.push($.create("li", {}, e));
		});
		return $.create("ul", { "class": className }, cmds);
	}

	this.appendTo = function (id) {
		$(id).append(that.createList(that.commands, "navlist"));
	}
	var that = this;
}

// -----------------------------------------
// Object: Text input/output manager
var TextWindow = {
	isOpen: false,
	
	// Just show a message, with a close button
	display: function (text, buttons) {
		if (buttons != null)
			TextWindow.show(text, buttons);
		else
			TextWindow.show(text, []);			
	},

	text: function () {
		return $("#textbox").val();
	},
	
	// Show the window	
	show: function (text, buttons) {
		if (this.isOpen == false) {
			buttons.push({label: "close", command: "TextWindow.close()", title: "Cancel or close"});
			buttons.each( function (i, btn) {
				$("#windowCommands").append(XHTML.button(btn));
			});
			$("#textWindow").show("slow");
			$("#textbox").val(text);
			$("#textbox").select();
			TextWindow.isOpen = true;
		}
	},
	
	// Close the window
	close: function () {
		$("#windowCommands").empty();
		$("#textWindow").hide("slow");
		this.isOpen = false;
	},

	select: function () {
		$("#textbox").select();
	}
}

// -----------------------------------------
// Object: Unique note id generator
var uniqueId = {
	next: function () {
		return "n" + Math.ran(0, 1000000);
	}
}

// -----------------------------------------
// Class: Note
function Note(text, colour, x, y) {
	this.id = uniqueId.next();
	this.text = text || "(empty)";
	this.colour = colour || Fuchsia.prefs.defaultColour;
	this.x = x || (Math.ran(1, 8) * Fuchsia.prefs.grid);
	this.y = y || (Math.ran(1, 8) * Fuchsia.prefs.grid);
	var that = this;

	// Create the HTML note
	this.newWidget = function () {
		var id = this.id;
		
		// Add the note commands
		var noteCommands = new CommandMenu([
			{ label: ">", title: "Command", list: [
				{ label: "colour", command: "Fuchsia.Note.changeColour", title: "Select the background colour" },
				{ label: "delete", command: "Fuchsia.Note.remove", title: "Delete and remove this note" },
			]}
		], id);
		var p2 = $.create("p", { "class": "note-commands" }, []);
		noteCommands.appendTo(p2, id);
		
		var p1 = $.create("p", { "id": id + "t" }, [this.text]);
		$(p1).editable(function (value, settings) {
			return(value); 
		}, { 
			event: "dblclick", 
			style: "inherit",
			tooltip: "Double click to edit note text",
			callback: function (value, settings) { that.text = value; },
		});	
		var div = $.create("div", { "class": "note", "id": id }, [p1, p2]);
		return div;
	}

	// Attach the note to the page
	this.attachToPage = function () {
		var div = this.newWidget();
		$("#notes").append(div);

		var my = this;
		var initPos = false;
		var collection = false;

		this.moveTo(this.x, this.y);
		this.setColour(this.colour);

		// $("div.noteCanvas").selectable({ });
		$('div.note').draggable({
			grid: [10, 10],
			// opacity: 0.5,
			helper: 'original',
			start: function(e, ui) {
				if ($(this).is('div.ui-selected')) {
					initPos = { x: parseInt(this.style.left, 10), y:parseInt(this.style.top, 10) };
					collection = jQuery('div.ui-selected:visible').not(this);
					if (collection.size() == 0) {
						initPos = false;
						collection = false;
					}
				}
			},
			drag: function(e, ui) {
				if (collection) {
					var x = ui.position.left;
					var y = ui.position.top;
					collection.each( function() {
						this.style.left = parseInt(this.style.left, 10) + x - initPos.x + 'px';
						this.style.top = parseInt(this.style.top, 10) + y - initPos.y + 'px';
					});
					initPos = {x:x, y:y};
				}
			},
			stop: function(e, ui) {
				initPos = false;
				collection = false;	
			}
		});
	}
	
	// Move the note relative to the bounding element's position
	this.moveTo = function (left, top) {
		var y_offset = 30;
		$("#" + this.id).animate( { left: left, top: top+y_offset }, "slow" );
		// $("#" + this.id).css({left: left, top: top+y_offset});
		this.x = left;
		this.y = top+y_offset;
	}

	this.getXY = function () {
		return {x: $("#"+this.id).css('left'), y: $("#"+this.id).css('top')};
	}
	
	this.setColour = function (c) {
		$("#" + this.id).css('background-color', c);
		var rgb = rgbHexToArray(c);
		var lum = rgb[0]*0.35 + rgb[1]*0.45 + rgb[2]*0.2;
		// alert(lum);
		if (lum < 110)
			$("#" + this.id + "t").css('color', "#ffffff");
		else
			$("#" + this.id + "t").css('color', "#000000");			
		this.colour = c;	
	}
	
	this.cycleColour = function () {
		var c = this.colour;
		c = (c + 1) % Fuchsia.numColours;
		this.setColour(c);
	}
	
	// Delete the note
	this.detach = function () {
		$("#" + this.id).remove();	// commit suicide
	}
	
	// Return the parameters of the note in a hash
	this.params = function () {
		var params = Object.toJSON(this);
		return params;
	}	
}

// -----------------------------------------
// Class: PersistentNoteStore
function PersistentNoteStore(storeName) {
	var hasGears = true;
	try {
		var db = google.gears.factory.create('beta.database');
		db.open(storeName);
		db.execute('CREATE TABLE IF NOT EXISTS Notes(Key TEXT, Value TEXT)');
	} catch (e) {
		alert("Google gears not installed, so no snapshots available");
		hasGears = false;
	}
	var my = this;

	this.saveSnapshot = function(store) {
		if (hasGears) {
			my.deleteAll();
			store.eachNote(function(i, note) {
				my.insert(note.id, note);
			});
		}
	}
	
	this.loadSnapshot = function(store) {
		if (hasGears) {
			arr = my.getAll();
			$.each(arr, function(i, elt) {
				var note = store.createNote(elt.text, elt.colour, elt.x, elt.y - 30);
			});
			store.applyPrefs();
		}
	}
	
	// Private functions
	
	// Insert a new key-value pair
	this.insert = function (key, value) {
		db.execute('INSERT INTO Notes values (?, ?)', [Object.toJSON(key), Object.toJSON(value)]);
	}
	
	// Delete a key-value pair
	this.remove = function (key) {
		db.execute('DELETE FROM Notes WHERE id=?', [Object.toJSON(key)]);
	}
	
	// Return the first value for a given key
	this.get = function (key) {
		var x;
		var rs = db.execute('SELECT * FROM Notes WHERE Key = ?', [Object.toJSON(key)]);
		if (rs.isValidRow())
			x = rs.field(1).parseJSON();
		rs.close();
		return x;
	}
	
	// Return all the entries
	this.getAll = function () {
		var all = [];
		var rs = db.execute('SELECT * FROM Notes');
		while (rs.isValidRow()) {
			all.push(rs.field(1).parseJSON());
			rs.next();
		}
		rs.close();
		return all;
	}
	
	// Clear the store
	this.deleteAll = function () {
		db.execute('DROP TABLE Notes');
		db.execute('CREATE TABLE IF NOT EXISTS Notes(Key TEXT, Value TEXT)');
	}	
}

// -----------------------------------------
// Class: NoteStore
function NoteStore() {
	// private member
	var notes = [];
	var my = this;
	
	// Create a new note, with a random position if not provided
	this.createNote = function (str, colour, x, y) {
		var c0 = colour || Fuchsia.prefs.defaultColour;
		var x0 = x || (Math.ran(1, 8) * Fuchsia.prefs.grid);
		var y0 = y || (Math.ran(1, 8) * Fuchsia.prefs.grid);
		var note = new Note(str, c0, x0, y0);
		notes.push(note);
		note.attachToPage();
		return note;
	}
	
	// Delete all notes
	this.deleteAll = function () {
		notes = [];
		$(".note").remove();
	}
		
	// Find a note by ID
	this.findById = function (id) {
		var x = jQuery.grep(notes, function (note, i) {
			return (id == note.id);
		});
		return x[0];
	}
	
	// Remove a note by ID
	this.removeById = function (id) {
		var n = my.findById(id);
		n.detach();
		notes = jQuery.grep(notes, function (note, i) {
			return (id != note.id);
		});
	}
	
	// Export all notes in JSON format
	this.exportJSON = function () {
		var arr = notes;
		return Object.toJSON(arr);
	}
	
	// Export all notes as an unsorted bulleted list
	this.exportText = function () {
		var strings = [];
		this.eachNote(function(i, note) {
			strings.push("* " + note.text);
		});		
		return (strings.join("\n"));
	}
	
	// Apply the global preferences to all notes
	this.applyPrefs = function () {
		var map = { 
			"width": Fuchsia.prefs.noteWidth,
			"font-size": Fuchsia.prefs.fontSize
		}
		$("div.note").css(map);
	}
	
	// Iterate over all the notes
	this.eachNote = function (fn) {
		$.each(notes, fn);
	}
	
	// Sort the note list by text
	this.sortByText = function () {
		notes.sort( function (a, b) {
			if (a.text < b.text) return -1;
			if (a.text > b.text) return 1;
			return 0;
		});
	}

	// Shuffle the note list
	this.shuffle = function () {
		this.eachNote(function(i, note) {
			$("#"+note.id).css("z-index", Math.ran(0, 99));
		});
		notes.sort( function (a, b) {
			return Math.ran(-1, 1);
		});
	}
}

// -----------------------------------------
// Main namespace: Object Fuchsia
var Fuchsia = {
	activeNotes: new NoteStore(),
	store: new PersistentNoteStore(),
	pageCommands: new CommandMenu([
		{ label: "<< add note", command: "Fuchsia.Note.add", title: "Add the note to the page" },
		{ label: "page...", title: "Page operations", list: [
			{ label: "save snapshot", command: "Fuchsia.Page.saveSnapshot", title: "Save to local disk" },
			{ label: "load snapshot", command: "Fuchsia.Page.loadSnapshot", title: "Load from local disk" },
			{ label: "--------" },
			{ label: "export as JSON", command: "Fuchsia.Page.exportJSON", title: "Export the text, state and position of all notes" },
			{ label: "export as text", command: "Fuchsia.Page.exportText", title: "Export the text as a bulleted list" },
			{ label: "import JSON...", command: "Fuchsia.Page.importJSON", title: "Import text and state of notes" },
			{ label: "import text...", command: "Fuchsia.Page.importText", title: "Import a list as notes" },
			{ label: "--------" },
			{ label: "set background colour", command: "Fuchsia.Page.bgcolor", title: "Set background colour (not sticky)" },
			]},
		{ label: "notes...", title: "Bulk operations on all the notes", list: [
			{ label: "add random word", command: "Fuchsia.NoteStore.addRandomWord", title: "Add a random word to the page" },
			{ label: "--------" },
			{ label: "lay out in grid", command: "Fuchsia.NoteStore.placeInGrid", title: "Lay out all the notes on a grid" },
			{ label: "stack", command: "Fuchsia.NoteStore.stackNotes", title: "Stack the notes in the top left corner" },
			{ label: "randomise positions", command: "Fuchsia.NoteStore.placeRandomly", title: "Randomly distribute the notes around the screen" },
			{ label: "shuffle", command: "Fuchsia.NoteStore.shuffle", title: "Shuffle the order of the notes" },
			{ label: "randomise colours", command: "Fuchsia.NoteStore.randomiseColours", title: "Give random colours to all notes" },
			{ label: "reset colours", command: "Fuchsia.NoteStore.resetColours", title: "Reset all notes back to their default color" },
			{ label: "sort by text", command: "Fuchsia.NoteStore.sortByText", title: "Sort the notes alphabetically" },
			{ label: "--------" },
			{ label: "set default colour", command: "Fuchsia.NoteStore.setDefaultColour", title: "Set the default note colour" },
			{ label: "delete all", command: "Fuchsia.NoteStore.deleteAll", title: "Remove all notes" },
			]},
		{ label: "about", command: "Fuchsia.about", title: "Information about Fuchsia" },
	]),
	numColours: 8,
	wordList: new RandomWordList(),
	
	// Global preferences
	prefs: {
		noteWidth: "110px",
		fontSize: "90%",
		defaultColour: "#ffff00",
		colours: [
			'transparent', '#ffffff','#d0d0d0','#777777','#000000', // monochromes
			'#ffaaaa','#ff00ff', '#ff0000','#aa0000','#9000ff', // reds
			'#ff6c00', '#ffff00', '#ffbb00', '#f0e68c','#d2b229', // browns/oranges/yellows
			'#aaffaa','#00ff00','#00aa00','#6b8e23','#007700', // greens
			'#bbddff','#00ffdd', '#aaaaff','#0000ff','#0000aa' // blues
			],
		grid: 10,
		topLeft: { x: 15, y: 90 },
	},
	
	// Set up
	initialise: function () {
		// alert("Running Fuchsia.initialise");
		Info.appendTo("#heading");
		Fuchsia.pageCommands.appendTo("#globalCommands");
		$("#textWindow").hide();
		$("#aboutWindow").hide();
		$("#colorpicker").hide();
		Fuchsia.activeNotes.applyPrefs();
		// alert("The fuchsia has landed.");
		Fuchsia.setFocus();
	},
		
	// Set the focus to the text field
	setFocus: function () {
		$("#newText").focus();
		$("#newText").select();
 	},
	
	// ---------------------------
	// Note commands
	Note: {
		// Add a note using the text from the field
		add: function (str) {
			var text = str || $("#newText").val();
			var note = Fuchsia.activeNotes.createNote(text);
			Fuchsia.activeNotes.applyPrefs();
			Fuchsia.setFocus();
		},
		remove: function (id) {
			Fuchsia.activeNotes.removeById(id);
		},
		changeColour: function (id) {
			var note = Fuchsia.activeNotes.findById(id);
			if (note == null)
				alert("Error: can't find note with id: " + id);
			else {
				$('#colorpicker').addColorPicker({
					clickCallback: function(c) {
						note.setColour(c);
						$('#colorpicker').empty().hide();
					}
				});
				$('#colorpicker').show('medium');
				
			}
		},
		// Callback from edit-in-place
		update: function (id, text) {
			var note = Fuchsia.activeNotes.findById(id);
			note.text = text;
		},
	},
	
	// ---------------------------
	// NoteStore commands
	NoteStore: {
		placeRandomly: function () {
			Fuchsia.activeNotes.eachNote( function (i, note) {
				note.moveTo(
					Math.ran(0, $(window).width() * 0.85), 
					Math.ran(0, $(window).height() * 0.75)
				);
			})
		},
		placeInGrid: function () {
			var x = 0;
			var y = 0;
			var x_spacing = $(".note").width() + 25;
			var y_spacing = $(".note").height() + 25;
			// alert(x_spacing + ", " + y_spacing);
			Fuchsia.activeNotes.eachNote( function(i, note) {
				note.moveTo(x, y);
				y += $("#" + note.id).height() + 25;
				if (y > $(window).height() * 0.75) {
					x += x_spacing;
					y = 0;
				}
			});
		},
		randomiseColours: function () {
			Fuchsia.activeNotes.eachNote( function (i, note) {
				note.setColour(Fuchsia.prefs.colours.random());
			});
		},
		resetColours: function () {
			Fuchsia.activeNotes.eachNote( function (i, note) {
				note.setColour(Fuchsia.prefs.defaultColour);
			});		
		},
		stackNotes: function () {
			Fuchsia.activeNotes.eachNote( function(i, note) {
				var x0 = Math.ran(1, 8) * Fuchsia.prefs.grid;
				var y0 = Math.ran(1, 8) * Fuchsia.prefs.grid;
				note.moveTo(x0, y0);
			});
		},
		sortByText: function () {
			Fuchsia.activeNotes.sortByText();
			alert("The notes have been sorted. Export or place in a grid to see the result.");
		},
		shuffle: function () {
			Fuchsia.activeNotes.shuffle();
			alert("The notes have been shuffled. Export or place in a grid to see the result.");
		},
		setDefaultColour: function () {
			$("#colorpicker").addColorPicker({
				clickCallback: function(c) { 
					Fuchsia.prefs.defaultColour = c;
					$("#colorpicker").empty().hide();
					Fuchsia.setFocus();
				}
			});		
			$('#colorpicker').show();		
		},	
		addRandomWord: function() {
			Fuchsia.Note.add(Fuchsia.wordList.next(), 0);
			return true;
		},		
		deleteAll: function () {
			if (confirm("Are you sure you want to delete all notes? Click OK to continue.")) {
				Fuchsia.activeNotes.deleteAll();
				// Fuchsia.store.deleteAll();
			}
			Fuchsia.setFocus();
		},
	},
	
	// ---------------------------
	// Page commands
	Page: {
		// Generate JSON to a separate window
		exportJSON: function () {
			TextWindow.display(Fuchsia.activeNotes.exportJSON(), []);
		},
	
		exportText: function () {
			TextWindow.display(Fuchsia.activeNotes.exportText(), []);
		},
		
		// Import JSON text
		importJSON: function () {
			TextWindow.display(null, [ { label: "import", title: "Click to import", command: "Fuchsia.Page.parseJSON()" } ] );
		},
		
		// Parse JSON text into notes
		parseJSON: function () {
			var obj = TextWindow.text().parseJSON();
			if (obj == null) {
				alert("Error: import text not understood");
				return false;
			}
			var arr = eval(obj);
			arr.each( function (i, elt) {
				if (typeof(elt.colour) == "number") // Migrate old versions
					elt.colour = Fuchsia.prefs.colours[elt.colour+1];
				Fuchsia.activeNotes.createNote(elt.text, elt.colour, elt.x, (elt.y - 30));
			});
			TextWindow.close();
			Fuchsia.activeNotes.applyPrefs();
		},
		
		saveSnapshot: function() { Fuchsia.store.saveSnapshot(Fuchsia.activeNotes); },
		loadSnapshot: function() { Fuchsia.store.loadSnapshot(Fuchsia.activeNotes); },
		
		// Import a list of text items
		importText: function () {
			TextWindow.display(null, [ { label: "import", title: "Click to import", command: "Fuchsia.Page.parseTextList()" } ] );
		},
		
		// Parse a text list
		parseTextList: function () {
			var items = TextWindow.text().split(/\n/);
			items.each( function (i, text) {
				text = text.replace(/^\s*[-*#o]\s/, "");
				if (text.length > 0) {
					Fuchsia.Note.add(text);
				}
			});
			TextWindow.close();
			Fuchsia.activeNotes.applyPrefs();
		},
		
		bgcolor: function () {
			$("#colorpicker").addColorPicker({
				clickCallback: function(c) { 
					$('body').css('background-color', c);
					$("#colorpicker").empty().hide();
					Fuchsia.setFocus();
				}
			});		
			$('#colorpicker').show();
		},
		
	},
		
	// Display info about the app
	about: function () {
		$("#aboutWindow").html(Info.aboutText());
		$("#aboutWindow").append(
			$.create("span", { "class" : "button", "onclick": "$('#aboutWindow').hide('medium')" }, ["OK"]));
		$("#aboutWindow").show("medium");
	},	
}	
	
// Set load point
$(document).ready(function() {
	Fuchsia.initialise()
});


// -----------------------------------------
// Stop premature exit
$(window).onbeforeunload = function () {
	return "You will lose all your notes if you do, unless you have saved them in a snapshot or exported them and pasted into a file.";
};

// The End
